﻿// Copied with slight modifications and improvements from Jim Lamb's post at: 
// http://blogs.msdn.com/b/jimlamb/archive/2009/11/18/how-to-create-a-custom-workflow-activity-for-tfs-build-2010.aspx
namespace TfsCloudDeploy.Workflow.Core.Activities
{
    using System;
    using System.Activities;
    using System.IO;
    using System.Text.RegularExpressions;
    using Microsoft.TeamFoundation.Build.Client;
    using Microsoft.TeamFoundation.Build.Workflow.Activities;
    using Microsoft.TeamFoundation.Build.Workflow.Tracking;
    using System.Collections.Generic;

    public enum LanguageType
    {
        CSharp,
        VisualBasic,
        CPlusPlus,
        Unsupported
    }

    /// <summary>
    /// Workflow activity that replaces all occurrences of a regular expression
    /// in a text file with the specified text.
    /// </summary>
    [BuildActivity(HostEnvironmentOption.Agent)]
    public sealed class ReplaceInFile : CodeActivity
    {
        #region Constants

        private const string AssemblyInfoMetadataItemRegularExpressionSearchPattern = @"(?<Prefix>[\[|\<]assembly:\s*{0}(?:Attribute)?\()(?<AttributeValue>[^)]*)(?<Suffix>\)[\]|\>])";
        private const string AssemblyInfoMetadataItemRegularExpressionReplacementPattern = "$1{0}$3";
        private const string CSharpAssemblyTemplate = "[assembly: {0}({1})]";
        private const string VisualBasicAssemblyTemplate = "<Assembly: {0}({1})>";
        private const string CPlusPlusAssemblyTemplate = "[assembly:{0}Attribute({1})]";

        #endregion

        #region Properties

        /// <summary>
        /// Specify the path of the file to replace occurences of the regular 
        /// expression with the replacement text.
        /// </summary>
        [RequiredArgument]
        public InArgument<string> FilePath
        {
            get;
            set;
        }

        /// <summary>
        /// Specify the version number.
        /// </summary>
        public InArgument<string> VersionNumber
        {
            get;
            set;
        }

        /// <summary>
        /// Regular expression to search for and replace in the specified
        /// text file.
        /// </summary>
        [RequiredArgument]
        public InArgument<List<string>> AssemblyInfoMetadata
        {
            get;
            set;
        }

        #endregion Properties

        #region Methods

        /// <summary>
        /// Executes the logic for this workflow activity
        /// </summary>
        /// <param name="context">Workflow context</param>
        /// <exception cref="ArgumentException">If the specified context does not
        /// contain a FilePath and a RegularExpression, an ArgumentException 
        /// is thrown.</exception>
        protected override void Execute(CodeActivityContext context)
        {
            // get the value of the FilePath in argument from the workflow context
            String filePath = FilePath.Get(context);

            // throw if the file path is null or empty
            if (String.IsNullOrEmpty(filePath))
            {
                throw new ArgumentException(
                    "[ReplaceInFile] Specify a path to replace text in.", "FilePath");
            }

            // throw if the specified file path doesn't exist
            if (!File.Exists(filePath))
            {
                throw new FileNotFoundException(
                    "[ReplaceInFile] File not found.", filePath);
            }

            List<String> assemblyInfoMetadata = context.GetValue(AssemblyInfoMetadata);
            if (assemblyInfoMetadata == null || assemblyInfoMetadata.Count == 0)
            {
                throw new ArgumentException(
                    "[ReplaceInFile] Specify a AssemblyInfoMetadata list.", "AssemblyInfoMetadata");
            }

            string versionNumber = context.GetValue(VersionNumber);
            IBuildDetail buildDetail = context.GetExtension<IBuildDetail>();

            // perform the actual replacement
            String contents = File.ReadAllText(filePath);
            bool changesWereMade = false;

            try
            {
                CreateBuildMessage(string.Format("[ReplaceInFile] File Path: {0}.", filePath), context);
                //CreateBuildMessage(string.Format("[ReplaceInFile] AssemblyInfoMetadata(count): {0}.", assemblyInfoMetadata.Count), context);

                foreach (var metadataItem in assemblyInfoMetadata)
                {
                    string[] metadataItemArray = metadataItem.Split(new char[] { '|' }, StringSplitOptions.RemoveEmptyEntries);

                    if (metadataItemArray.Length != 2)
                    {
                        throw new ArgumentException(
                            "[ReplaceInFile] AssemblyInfoMetadata list entries should be in the form: <attribute-name>|<value>.", "AssemblyInfoMetadata");
                    }

                    string metadataItemKey = metadataItemArray[0];
                    string metadataItemValue = metadataItemArray[1];

                    CreateBuildMessage(string.Format("[ReplaceInFile] AttributeName: {0} | AttributeValue: {1}", metadataItemKey, metadataItemValue), context);

                    string regexPattern = string.Format(AssemblyInfoMetadataItemRegularExpressionSearchPattern, metadataItemKey);
                    Regex metadataRegex = new Regex(regexPattern, RegexOptions.IgnoreCase | RegexOptions.Multiline);

                    try
                    {
                        MatchCollection matches = metadataRegex.Matches(contents);

                        if (matches.Count == 0)
                        {
                            // Add an entry to the end of the file if no match is found for the assembly attribute:
                            string newAttributeEntry = CreateAssemblyAttributeStatement(filePath, metadataItemKey, metadataItemValue);
                            if (!string.IsNullOrWhiteSpace(newAttributeEntry))
                            {
                                contents = string.Format("{0}\r\n{1}", contents, newAttributeEntry);
                            }
                        }

                        contents = metadataRegex.Replace(contents, string.Format(AssemblyInfoMetadataItemRegularExpressionReplacementPattern, string.Format(ReplacePredefinedVariables(metadataItemValue, versionNumber, buildDetail), "$2")));
                    }
                    catch (Exception ex)
                    {
                        throw new Exception(string.Format("[ReplaceInFile] An exception occurred while processing '{0}' with value '{1}'. Please review specified value and make sure you are not missing any double quotes." +
                            "Error Message: {2}", metadataItemKey, metadataItemValue, ex.Message), ex);
                    }

                    changesWereMade = true;
                }
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("[ReplaceInFile] An exception occurred. Error Message: {0}. StackTrace: {1}", ex.Message, ex.StackTrace), ex);
            }

            if (changesWereMade)
            {
                // ensure that the file is writeable
                FileAttributes fileAttributes = File.GetAttributes(filePath);
                File.SetAttributes(filePath, fileAttributes & ~FileAttributes.ReadOnly);

                CreateBuildMessage(string.Format("[ReplaceInFile] Writing modified file back to: {0}.", filePath), context);
                File.WriteAllText(filePath, contents);

                // restore the file's original attributes
                File.SetAttributes(filePath, fileAttributes);
            }
        }

        private void CreateBuildMessage(string messageText, CodeActivityContext context)
        {
            context.Track(new BuildInformationRecord<BuildMessage>()
            {
                Value = new BuildMessage()
                {
                    Importance = BuildMessageImportance.High,
                    Message = messageText,
                },
            });
        }

        private string CreateAssemblyAttributeStatement(string filePath, string metadataItemKey, string metadataItemValue)
        {
            LanguageType languageType = GetLanguageTypeForFile(filePath);

            switch(languageType)
            {
                case LanguageType.CSharp:
                    return string.Format(CSharpAssemblyTemplate, metadataItemKey, metadataItemValue);
                case LanguageType.VisualBasic:
                    return string.Format(VisualBasicAssemblyTemplate, metadataItemKey, metadataItemValue);
                case LanguageType.CPlusPlus:
                    return string.Format(CPlusPlusAssemblyTemplate, metadataItemKey, metadataItemValue);
                default:
                    return null;
            }
        }

        private LanguageType GetLanguageTypeForFile(string filePath)
        {
            if (filePath.EndsWith(".cs"))
            {
                return LanguageType.CSharp;
            }
            else if (filePath.EndsWith(".vb"))
            {
                return LanguageType.VisualBasic;
            }
            else if (filePath.EndsWith(".cpp"))
            {
                return LanguageType.CPlusPlus;
            }
            else
            {
                return LanguageType.Unsupported;
            }
        }

        private string ReplacePredefinedVariables(string metadataValue, string versionNumber, IBuildDetail buildDetail)
        {
            if (string.IsNullOrWhiteSpace(metadataValue))
                return metadataValue;
            
            return metadataValue
                .Replace("[VersionNumber]", versionNumber)
                .Replace("[BuildName]", buildDetail.BuildDefinition.Name)
                .Replace("[BuildNumber]", buildDetail.BuildNumber)
                .Replace("[LabelName]", buildDetail.LabelName)
                .Replace("[Quality]", buildDetail.Quality)
                .Replace("[RequestedBy]", buildDetail.RequestedBy)
                .Replace("[RequestedFor]", buildDetail.RequestedFor)
                .Replace("[ShelvesetName]", buildDetail.ShelvesetName)
                .Replace("[SourceGetVersion]", buildDetail.SourceGetVersion)
                .Replace("[TeamProject]", buildDetail.TeamProject)
                .Replace("[StartTime]", string.Format("{0} {1}", buildDetail.StartTime.ToShortDateString(), buildDetail.StartTime.ToShortTimeString()));
        }

        #endregion Methods
    }
}